Go 基础教程
Go 基础教程
[TOC]
GO 包管理
第一个Go程序及其分析
新建一个文件 main.go,写入
package main
import "fmt"
func main() {
fmt.Println("Hello World!")
}
执行go run main.go 或 go run .,将会输出
$ go run .
Hello World!
- package main:
- 声明了 main.go 所在的包,Go 语言中使用包来组织代码。一般一个文件夹即一个包,包内可以暴露类型或方法供其他包使用。
- import “fmt”:
- fmt 是 Go 语言的一个标准库/包,用来处理标准输入输出。
- func main:
- main 函数是整个程序的入口,main 函数所在的包名也必须为
main。 - fmt.Println(“Hello World!”):
- 调用 fmt 包的 Println 方法,打印出 “Hello World!”
go run main.go,其实是 2 步:
go build main.go:编译成二进制可执行程序
./main:执行该程序
Go 语言的基础组成有以下几个部分:
- 包声明
- 引入包
- 函数
- 变量
- 语句 & 表达式
- 注释
变量与内置数据类型
3.1变量
Go语言是静态类型的,因此声明变量时候必须明确变量类型
在 Go 语言中,= 和 := 是两个不同的赋值操作符。
= 是赋值操作符,用于给变量赋值。例如:
a := 10
b := a
a = 20
fmt.Println(a, b) // 输出:20 10
在上面的代码中,= 用于给变量 a 和 b 赋值。
:= 是声明并赋值操作符,用于声明新的变量并赋值。例如:
a := 10
b := a
c := "hello"
fmt.Println(a, b, c) // 输出:10 10 hello
在上面的代码中,:= 用于声明变量 c 并给它赋值。
需要注意的是,:= 只能在函数内部使用,用于声明新的变量。如果变量已经被声明过,就不能再使用 := 来赋值,而应该使用 =。例如:
func main() {
a := 10
a := 20 // 编译错误:no new variables on left side of :=
a = 20 // 正确
}
在上面的代码中,第二个赋值语句会导致编译错误,因为变量 a 已经被声明过,不能再使用 := 来声明。正确的做法是直接使用 = 来赋值。
Go 语言与其他语言显著不同的一个地方在于,Go 语言的类型在变量后面。比如 java 中,声明一个整体一般写成 int a = 1,在 Go 语言中,需要这么写:
var a int // 如果没有赋值,默认为0
var a int = 1 // 声明时赋值
var a = 1 // 声明时赋值
var a = 1,因为 1 是 int 类型的,所以赋值时,a 自动被确定为 int 类型,所以类型名可以省略不写,这种方式还有一种更简单的表达:
a := 1
msg := "Hello World!"
3.2 简单类型
空值:nil
整型类型: int(取决于操作系统), int8, int16, int32, int64, uint8, uint16, …
浮点数类型:float32, float64
字节类型:byte (等价于uint8)
字符串类型:string
布尔值类型:boolean,(true 或 false)
var a int8 = 10
var c1 byte = 'a'
var b float32 = 12.2
var msg = "Hello World"
ok := false
3.3 字符串
在 Go 语言中,字符串使用 UTF8 编码,UTF8 的好处在于,如果基本是英文,每个字符占 1 byte,和 ASCII 编码是一样的,非常节省空间,如果是中文,一般占3字节。包含中文的字符串的处理方式与纯 ASCII 码构成的字符串有点区别。
我们看下面的例子:
package main
import (
"fmt"
"reflect"
)
func main() {
str1 := "Golang"
str2 := "Go语言"
fmt.Println(reflect.TypeOf(str2[2]).Kind()) // uint8
fmt.Println(str1[2], string(str1[2])) // 108 l
fmt.Printf("%d %c\n", str2[2], str2[2]) // 232 è
fmt.Println("len(str2):", len(str2)) // len(str2): 8
}
- eflect.TypeOf().Kind() 可以知道某个变量的类型,我们可以看到,字符串是以 byte 数组形式保存的,类型是 uint8,占1个 byte,打印时需要用 string 进行类型转换,否则打印的是编码值。
- 因为字符串是以 byte 数组的形式存储的,所以,
str2[2]的值并不等于语。str2 的长度len(str2)也不是 4,而是 8( Go 占 2 byte,语言占 6 byte)。
正确的处理方式是将 string 转为 rune 数组
str2 := "Go语言"
runeArr := []rune(str2)
fmt.Println(reflect.TypeOf(runeArr[2]).Kind()) // int32
fmt.Println(runeArr[2], string(runeArr[2])) // 35821 语
fmt.Println("len(runeArr):", len(runeArr)) // len(runeArr): 4
转换成 []rune 类型后,字符串中的每个字符,无论占多少个字节都用 int32 来表示,因而可以正确处理中文。
3.4 数组(array)与切片(slice)
声明数组
var arr [5]int // 一维
var arr2 [5][5]int // 二维
声明时初始化
var arr = [5]int{1, 2, 3, 4, 5}
// 或 arr := [5]int{1, 2, 3, 4, 5}
使用 [] 索引/修改数组
arr := [5]int{1, 2, 3, 4, 5}
for i := 0; i < len(arr); i++ {
arr[i] += 100
}
fmt.Println(arr) // [101 102 103 104 105]
数组的长度不能改变,如果想拼接2个数组,或是获取子数组,需要使用切片。切片是数组的抽象。 切片使用数组作为底层结构。切片包含三个组件:容量,长度和指向底层数组的指针,切片可以随时进行扩展
声明切片:
slice1 := make([]float32, 0) // 长度为0的切片
slice2 := make([]float32, 3, 5) // [0 0 0] 长度为3容量为5的切片
fmt.Println(len(slice2), cap(slice2)) // 3 5
使用切片:
// 添加元素,切片容量可以根据需要自动扩展
slice2 = append(slice2, 1, 2, 3, 4) // [0, 0, 0, 1, 2, 3, 4]
fmt.Println(len(slice2), cap(slice2)) // 7 12
// 子切片 [start, end)
sub1 := slice2[3:] // [1 2 3 4]
sub2 := slice2[:3] // [0 0 0]
sub3 := slice2[1:4] // [0 0 1]
// 合并切片
combined := append(sub1, sub2...) // [1, 2, 3, 4, 0, 0, 0]
- 声明切片时可以为切片设置容量大小,为切片预分配空间。在实际使用的过程中,如果容量不够,切片容量会自动扩展。
sub2...是切片解构的写法,将切片解构为 N 个独立的元素
3.5 字典(键值对,map)
map 类似于 java 的 HashMap,Python的字典(dict),是一种存储键值对(Key-Value)的数据解构。使用方式和其他语言几乎没有区别。
// 仅声明
m1 := make(map[string]int)
// 声明时初始化
m2 := map[string]string{
"Sam": "Male",
"Alice": "Female",
}
// 赋值/修改
m1["Tom"] = 18
3.6 指针(pointer)
指针即某个值的地址,类型定义时使用符号*,对一个已经存在的变量,使用 & 获取该变量的地址。
str := "Golang"
var p *string = &str // p 是指向 str 的指针
*p = "Hello"
fmt.Println(str) // Hello 修改了 p,str 的值也发生了改变
一般来说,指针通常在函数传递参数,或者给某个类型定义新的方法时使用。Go 语言中,参数是按值传递的,如果不使用指针,函数内部将会拷贝一份参数的副本,对参数的修改并不会影响到外部变量的值。如果参数使用指针,对参数的传递将会影响到外部变量。
例如:
func add(num int) {
num += 1
}
func realAdd(num *int) {
*num += 1
}
func main() {
num := 100
add(num)
fmt.Println(num) // 100,num 没有变化
realAdd(&num)
fmt.Println(num) // 101,指针传递,num 被修改
流程控制(if, for, switch)
Go 网络工程与包管理
Go语言的并发性能
go语言具有出色的并行能力,提升运行效率 并发是指系统中同时执行多个独立的任务或操作的能力,这些任务可以在相同的时间段内被执行,但并不一定同时执行。 并行是指在同一时间段内同时执行多个任务或操作的能力。在并行系统中,多个任务可以同时执行,每个任务可以在不同的处理器核心或计算机上执行。
Goroutine:
1.轻量级:Goroutine的栈空间只占用少量内存,初始大小只有2K,可以根据需要动态增长或缩小。 2.快速:Goroutine的创建、销毁、切换和调度都非常快,远远快于操作系统线程。 3.并发:Goroutine可以轻松地实现并发编程,可以使用go关键字创建Goroutine并让它们在不同的核心上运行,从而充分利用多核处理器的性能。 4.通信:Goroutine之间可以通过通道进行通信,通道是一种特殊的数据结构,可以实现Goroutine之间的同步和数据交换。 5.调度:Goroutine的调度器是Go语言运行时环境中的一部分,它会自动调度Goroutine的执行,确保它们充分利用CPU资源。 6.安全:Goroutine的调度和内存管理都由运行时环境管理,可以避免常见的并发编程错误,如死锁和竞态条件。
#### 二、CSP(communicating sequential process)
这是一种并发编程模型,它由Tony Hoare于1978年提出,旨在通过通信来实现并发。CSP模型由一组并发执行的进程组成,每个进程都是独立的,各自执行自己的任务,通过通信来协调它们之间的交互和协作。 在CSP模型中,进程之间通过通道(channel)进行通信。通道是一种先进先出的消息队列,进程可以向通道中发送消息,并从通道中接收消息。通道可以实现进程之间的同步和数据交换,防止了竞争状态和死锁等并发编程常见问题。 CSP模型的主要内容包括: 进程:CSP模型由一组并发执行的进程组成,每个进程都是独立的,各自执行自己的任务。 通道:通道是一种先进先出的消息队列,进程可以向通道中发送消息,并从通道中接收消息。 通信:进程之间通过通道进行通信,通道可以实现进程之间的同步和数据交换。 同步:通道可以用于进程之间的同步,一个进程可以等待另一个进程发送消息到通道中,从而实现进程之间的同步。 数据交换:通道可以用于进程之间的数据交换,一个进程可以向通道中发送消息,另一个进程可以从通道中接收消息,从而实现进程之间的数据交换。 CSP模型具有简单、清晰、安全和高效等优点,可以帮助开发人员轻松地实现高效、可扩展和安全的并发程序。在Go语言中,CSP模型被广泛应用,通过goroutine和channel的结合,实现了一种高效、安全、简单的并发编程模型。
三、在并发编程中,通信共享内存和共享内存实现通信是两种不同的编程模型,它们的实现方式和设计思想有所不同。
共享内存实现通信是指多个线程或进程通过读写同一块共享内存来实现数据交换和同步。在这种模型中,线程或进程之间可以直接访问共享内存中的数据,但需要使用锁等同步机制来避免数据竞争和死锁等问题。这种模型的优点是数据访问速度快,但缺点是需要使用复杂的同步机制来避免并发问题,并且难以扩展到分布式系统中。 通信共享内存是指多个线程或进程之间通过通信来实现数据交换和同步。在这种模型中,线程或进程之间通过发送和接收消息来进行通信,而不是直接访问共享内存。这种模型的优点是易于实现和维护,并且可以扩展到分布式系统中,但缺点是通信的开销较大,对于大规模数据处理和计算密集型任务可能会影响性能。 因此,通信共享内存更加注重数据的安全和可扩展性,而共享内存实现通信更加注重数据的访问速度和性能。在实际应用中,选择哪种模型取决于具体的应用场景和需求。例如,在多核计算机上进行并行计算时,共享内存实现通信可能更加有效,而在分布式系统中进行数据交换时,通信共享内存可能更加适合。 四、缓冲通道的主要作用包括:
解耦发送和接收操作:在非缓冲通道中,发送和接收操作必须同时发生,否则会阻塞程序。而在缓冲通道中,发送和接收操作可以在不同的时间进行,发送方可以将元素缓存在通道中,而接收方可以在需要时从通道中接收元素。
提高并发性能:缓冲通道可以减少发送和接收操作之间的等待时间,从而提高程序的并发性能。例如,一个生产者可以将元素缓存在通道中,而多个消费者可以并发地从通道中接收元素,从而提高整个系统的吞吐量。
控制通道容量:缓冲通道可以控制通道中元素的数量,从而避免内存泄漏和资源浪费。如果通道中的元素数量达到通道容量的上限,发送操作将被阻塞,直到通道中有空间可以缓存新的元素。
需要注意的是,缓冲通道的使用也会带来一定的风险。如果通道中的缓存元素过多,可能会导致程序的内存占用过大,从而影响程序的性能和稳定性。因此,在使用缓冲通道时,需要根据具体的应用场景和需求,合理设置通道容量,并确保通道中的元素数量不会过多,以避免不必要的开销和风险。
五、在Go语言中,defer关键字用于延迟函数的执行,即将一个函数推迟到当前函数返回之前执行。defer语句可以在函数中的任何地方使用,并且可以推迟多个函数。
defer语句的主要作用包括:
延迟函数的执行:使用defer语句可以将一个函数推迟到当前函数返回之前执行,无论函数是正常返回还是异常返回。
简化资源管理:使用defer语句可以简化资源管理,例如在打开一个文件后,可以使用defer语句在函数返回前关闭该文件,避免资源泄漏。
调试和追踪:使用defer语句可以在函数执行过程中打印调试和追踪信息,帮助开发人员诊断问题。
需要注意的是,defer语句的执行顺序是后进先出的,即最后一个推迟的函数最先执行,而最先推迟的函数最后执行。 使用同步Mutex实现并发安全 依赖管理:
GOPATH:
在 Go 语言中,GOPATH 是一个环境变量,它指定了 Go 语言源代码、依赖包和可执行文件的路径。在 GOPATH 中,源代码和依赖包被组织在三个目录中:src、pkg 和 bin。
src 目录存放 Go 语言源代码,每个项目都应该有自己的一个目录,其中包含该项目的所有源代码文件和子目录。 pkg 目录存放编译后的包文件(.a 文件),每个目录对应一个特定的操作系统和处理器架构。 bin 目录存放可执行文件,例如 go install 命令生成的二进制文件。 在 GOPATH 中,可以有多个项目,每个项目都应该有自己的一个目录。
需要注意的是,从 Go 1.11 版本开始,Go 语言增加了对 Go modules 的支持,Go modules 允许开发人员在不依赖 GOPATH 的情况下管理依赖包。因此,对于使用 Go modules 的项目,GOPATH 可以被忽略。
如果您正在使用 Go 1.11 版本或更高版本,并且正在使用 Go modules 来管理依赖包,则可以不必设置 GOPATH 环境变量。如果您仍然在使用旧版本的 Go 或者不使用 Go modules,则应该设置 GOPATH 环境变量。 Go语言中的vendor机制可以解决依赖包管理的问题,它允许将依赖包复制到项目的vendor目录下,以便在不同的项目之间共享和管理依赖包。
使用vendor机制的步骤如下:
在项目根目录下创建vendor目录。
将需要使用的依赖包复制到vendor目录下,并按照包的路径组织目录结构。 在Go源文件中使用import语句导入依赖包,Go编译器会自动在vendor目录下查找依赖包。例如: import "github.com/gin-gonic/gin" 在使用go build、go run或go test等命令时,加上-vendor标志,以便让Go编译器使用vendor目录下的依赖包。例如: go build -v -mod=vendor go run -v -mod=vendor main.go go test -v -mod=vendor ./... 需要注意的是,使用vendor机制可能会引入一些问题,例如依赖包版本冲突、依赖包更新延迟等。因此,在使用vendor机制时,需要谨慎管理依赖包,避免出现问题。 从Go 1.14版本开始,Go语言增加了对Go module的支持,它是一种新的依赖包管理机制,可以更加高效地管理依赖包,而不需要依赖vendor目录。如果您使用的是Go 1.14或更高版本,并且使用Go module管理依赖包,则可以不必使用vendor机制。 因vendor无法控制依赖的版本,而且更新项目中容易出现依赖冲突,因此正在减少其使用
当你在使用 Go 语言编写代码时,你可能需要使用一些第三方库或者工具包。在 Go 语言中,这些库被称为模块。Go Module 是 Go 语言官方提供的一种包管理机制,可以方便地管理和维护你的代码与依赖。
Go Module 通过在项目中添加一个名为 go.mod 的文件来管理依赖。这个文件记录了你的项目所依赖的所有模块及其版本信息。当你需要引入一个新的第三方库时,你可以使用 go get 命令来下载该库,并将其添加到 go.mod 文件中。
在 go.mod 文件中,你可以指定每个模块的版本信息。Go Module 会根据这些版本信息来管理依赖关系,并确保每个依赖的模块都与你的代码兼容。如果你需要更新依赖的模块,你只需要修改 go.mod 文件中相应的版本信息,然后运行 go mod tidy 命令来更新依赖关系。
Go Module 还支持私有模块和本地模块。你可以将自己的代码发布为一个模块,并将其上传到一个私有的代码仓库或者本地的文件系统中。这样,在其他项目中,你就可以使用相同的方式来管理自己的模块依赖。
总之,Go Module 提供了一种简单、方便、可靠的方式来管理你的代码依赖。它可以帮助你更轻松地管理你的项目,并确保你的依赖关系始终保持最新和稳定。
总结下来,依赖管理有三要素:
1 配置文件描述依赖:go.mod
2 中心仓库管理依赖库:Proxy
3 本地工具: go get/mod
————依赖配置:go.mod

————依赖配置:version
在Go Modules依赖管理中,版本号分为两种形式:1. 语义化版本(Semantic Versioning):也就是常见的x.y.z形式的版本号,比如1.12.5。这种版本号遵循以下语义:- x:主版本号,当有不兼容的API变动时增加 - y:次版本号,当有向下兼容的功能增强时增加 - z:修订号,当有向下兼容的问题修复时增加
使用语义化版本号,可以根据版本号的变化判断依赖的升级会带来什么样的影响。
基于提交版本(Pseudo-Version):
这种版本号形如v0.0.0-yyyymmddhhmmss-abcdefg。这里的yyyymmddhhmmss是提交时间,abcdefg是提交的哈希值。
举个例子,v0.0.0-20200801131504-af253643a47a就是基于2020年8月1日13:15:04这个提交生成的版本。基于提交版本的好处是可以精确锁定依赖到某一次提交,以避免某些情况下的非兼容更新。
但是缺点是版本号长度比较长,并且不如语义化版本号易于人工解析。所以,Go Modules支持以上两种版本模式,可以根据场景选择:
开发阶段通常使用基于提交版本,以锁定依赖到known good的提交
发布阶段通常使用语义化版本,以便给用户更友好的版本体验
我们同时也可以在go.mod文件中同时使用这两种版本模式,Go会自动处理。
总之,这两种版本模式为我们提供了更灵活的依赖管理方案。
————依赖配置:indirect、incompatible
即间接依赖与【在require指令中,我们可以对依赖版本使用incompatible标注,表示该版本】存在不兼容的API更新。
当我们使用go get -u更新依赖时,Go Modules会跳过标注了incompatible的依赖。也就是说,使用incompatible标注可以避免某些依赖被非兼容更新,从而确保项目稳定
————依赖分发-Proxy方式
这意味着在下载依赖时,会通过一个中间代理服务器进行下载。 使用proxy分发依赖有以下几个主要特点:
- 避免直接访问外网:有些环境下无法直接访问外网,此时可以通过proxy,先将依赖下载到proxy服务器,然后再从proxy下载到本地。
- 缓存依赖:proxy服务器可以缓存常用依赖,节省重复下载时间,提高效率。
- 隔离外网:通过proxy可以在一定程度上隔离外网,避免外网的影响。
- 灵活控制:可以通过proxy灵活控制对特定依赖的访问,实现更严密的控制。 要使用Go Modules的proxy模式,可以通过以下步骤:
- 启动一个proxy服务器。通常可以使用Go内置的
go mod proxy命令启动一个proxy服务器。 - 在客户端设置
GOPROXY环境变量为proxy服务器地址。例如export GOPROXY=http://proxyserver。 - 可选地,还可以设置
GONOSUMDB环境变量来禁用校验和数据库。因为通过proxy,校验和就由proxy维护了。 - 运行
go mod tidy等命令下载依赖,此时会通过proxy服务器下载。 - 代理服务器需要定期运行
go mod download来更新缓存。 为了安全起见,Go 1.15版本开始,proxy服务器默认只允许从localhost和lan访问。如果需要外部访问,需要显式设置环境变量GONOSUMDB_ALL=* proxyserver=http://proxyserver。 所以,总体来说,使用Go Modules的proxy模式可以带来更严密的依赖控制,适用于特定的网络环境。但也需要维护proxy服务器,略微提高了复杂性。
工具:go get
go get example.org/pkg ……
go mod init/download/tidy